package com.example.demo.dynamicproxy.v2;

import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

/**
 * @author ZhenZhong
 * @date 2020/3/4
 */
public class DemoProxy {
    private static final String ln = "\r\n";
    private static final String indent = "\t";
    private static String proxyClassName = "$Proxy1";
    private static final Package packageName = DemoProxy.class.getPackage();

    public static final Object newProxyInstance(DemoClassLoader loader, Class<?>[] interfaces, DemoInvocationHandler handler) throws IOException {

        try {
            Class proxyClass = getProxy0(loader, interfaces);
            Constructor constructor = proxyClass.getConstructor(DemoInvocationHandler.class);
            return constructor.newInstance(handler);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;

    }

    private static void compileJavaFile(File file) throws IOException {
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        StandardJavaFileManager manager = compiler.getStandardFileManager(null, null, null);
        Iterable<? extends JavaFileObject> javaFileObjects = manager.getJavaFileObjects(file);

        JavaCompiler.CompilationTask task = compiler.getTask(null, null, null, null, null, javaFileObjects);
        task.call();
        manager.close();
    }

    private static File createProxyJavaFile(String source) throws IOException {
        String path = DemoProxy.class.getResource("").getPath();
        File file = new File(path + proxyClassName + ".java");
        FileWriter fileWriter = new FileWriter(file);
        fileWriter.write(source);
        fileWriter.flush();
        fileWriter.close();
        return file;
    }

    private static Class getProxy0(DemoClassLoader loader, Class<?>[] interfaces) throws IOException, ClassNotFoundException {

        String source = generateSrc(interfaces);
        File file = createProxyJavaFile(source);
        compileJavaFile(file);
        Class proxyClass = loader.findClass(getProxyClassName());
        return proxyClass;
    }

    private static String generateSrc(Class<?>[] interfaces) {
        StringBuilder builder = new StringBuilder();
        Class<?> anInterface = interfaces[0];
        builder.append(packageName).append(";").append(ln);
        builder.append("import java.lang.reflect.*;").append(ln);
        builder.append("public final class ").append(proxyClassName).append(" implements ").append(anInterface.getName()).append("{").append(ln);
        builder.append(indent).append(DemoInvocationHandler.class.getName()).append(" h;").append(ln);
        builder.append(indent).append("public ").append(proxyClassName).append("(").append(DemoInvocationHandler.class.getName()).append(" h){").append(ln);
        builder.append(indent).append(indent).append("this.h = h;").append(ln);
        builder.append(indent).append("}").append(ln);

        for (Method method : anInterface.getMethods()) {

            Class<?>[] parameterTypes = method.getParameterTypes();
            StringBuilder paramTypeName = new StringBuilder();
            StringBuilder paramTypeValue = new StringBuilder();
            StringBuilder paramTypeClass = new StringBuilder();
            for (int i = 0; i < parameterTypes.length; i++) {
                Class<?> parameterType = parameterTypes[i];
                String type = parameterType.getName();
                String paramName = toLowerFirstCase(parameterType.getSimpleName());
                paramTypeName.append(type + " " + paramName);
                paramTypeValue.append(paramName);
                paramTypeClass.append(parameterType.getName() + ".class");
                if (i < parameterTypes.length - 1) {
                    paramTypeName.append(",");
                    paramTypeValue.append(",");
                    paramTypeClass.append(",");
                }
            }

            builder.append(indent).append("public ").append(method.getReturnType().getName()).append(" ").append(method.getName()).append("(").append(paramTypeName).append("){").append(ln);
            builder.append(indent).append(indent).append("try{").append(ln);
            builder.append(indent).append(indent).append(indent).append("Method m = ").append(anInterface.getName()).append(".class.getMethod(\"").append(method.getName()).append("\", new Class[]{").append(paramTypeClass).append("});").append(ln);
            if (method.getReturnType() != void.class) {
                builder.append(indent).append(indent).append(indent).append("return (").append(method.getReturnType().getName()).append(")");
                builder.append(indent).append(indent).append(indent).append("(this.h.invoke(this, m, new Object[]{").append(paramTypeValue).append("}));").append(ln);
            } else {
                builder.append(indent).append(indent).append(indent).append("this.h.invoke(this, m, new Object[]{").append(paramTypeValue).append("});").append(ln);

            }

            builder.append(indent).append(indent).append("}catch(Error ex){").append(ln);
            builder.append(indent).append(indent).append("}catch(Throwable ex){").append(ln);
            builder.append(indent).append(indent).append(indent).append("throw new UndeclaredThrowableException(ex);").append(ln);
            builder.append(indent).append(indent).append("}").append(ln);
            if (method.getReturnType() != void.class) {
                builder.append(indent).append(indent).append("return null;").append(ln);
            }
            builder.append(indent).append(indent).append(ln);

            builder.append(indent).append("}").append(ln);


            builder.append(indent).append(ln);
            builder.append(indent).append(ln);
        }
        builder.append("}");
        System.out.println(builder);
        return builder.toString();
    }

    private static String toLowerFirstCase(String simpleName) {
        char[] chars = simpleName.toCharArray();
        chars[0] += 32;
        return new String(chars);
    }

    private static String getProxyClassName() {
        return packageName.getName() + "." + proxyClassName;
    }
}
